package org.examcity.util;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;

import org.apache.commons.beanutils.PropertyUtils;

/**
 * Utility methods to handle reflection.
 * 
 * @author Antonio Gomes
 */
public final class ReflectionUtil {

	private static final Map<Class<?>, Map<String, PropertyDescriptor>> propertyMap = new WeakHashMap<Class<?>, Map<String, PropertyDescriptor>>();
	private static final Map<FieldRefKey, Field> fieldMap = new WeakHashMap<FieldRefKey, Field>();

	private ReflectionUtil() {
		// no instance allowed
	}

	/**
	 * Gets the {@link PropertyDescriptor} for a given bean property.
	 * @param clazz
	 * @param property
	 * @return
	 */
	public static PropertyDescriptor getDescriptor(Class<?> clazz, String property) {
		synchronized (propertyMap) {
			// This map is used only to allow fast access to descriptors by name, as
			// commons-beanutils already caches reflection metadata
			Map<String, PropertyDescriptor> classProps = propertyMap.get(clazz);
			if (classProps == null) {
				classProps = new HashMap<String, PropertyDescriptor>();
				propertyMap.put(clazz, classProps);
				for (PropertyDescriptor pd : PropertyUtils.getPropertyDescriptors(clazz)) {
					classProps.put(pd.getName(), pd);
				}
			}
			return classProps.get(property);
		}
	}

	/**
	 * Convenience method that returns the class of an object even if it is a CGLIB proxy.
	 * @param o Object
	 * @return class or <code>null</code> if the parameter is <code>null</code>
	 */
	public static Class<?> getClass(Object o) {
		return (o == null) ? null : getNonProxiedClass(o.getClass());
	}

	/**
	 * Returns the original proxied class if the parameter points to a CGLIB proxy.
	 * @param clazz
	 * @return original class
	 */
	@SuppressWarnings("unchecked")
	public static <T> Class<T> getNonProxiedClass(Class<T> clazz) {
		if (isCglibProxy(clazz)) {
			return (Class<T>) clazz.getSuperclass();
		}
		return clazz;
	}

	/**
	 * Checks whether a given class is a CGLIB proxy or not.
	 * 
	 * @param clazz Class to test
	 * @return <code>true</code> if the class is a proxy
	 */
	public static boolean isCglibProxy(Class<?> clazz) {
		return clazz.getName().indexOf("$$EnhancerByCGLIB$$") != -1;
	}

	/**
	 * Gets the i-th parameter type of a class.
	 * @param clazz
	 * @return type
	 */
	public static Class<?> getParameterType(Class<?> clazz, int index) {
		ParameterizedType genericSuperclass = (ParameterizedType) clazz.getGenericSuperclass();
		return (Class<?>) genericSuperclass.getActualTypeArguments()[index];
	}

	public static void setProperty(Object bean, String property, Object value) {
		try {
			PropertyUtils.setProperty(bean, property, value);
		} catch (IllegalAccessException e) {
			throw new RuntimeException(e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException(e);
		} catch (NoSuchMethodException e) {
			throw new RuntimeException(e);
		}
	}

	public static Object getProperty(Object bean, String property) {
		try {
			return PropertyUtils.getProperty(bean, property);
		} catch (IllegalAccessException e) {
			throw new RuntimeException(e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException(e);
		} catch (NoSuchMethodException e) {
			throw new RuntimeException(e);
		}
	}

	public static void writeField(Object object, String field, Object value) {
		try {
			Field f = getField(object.getClass(), field);
			if (!f.isAccessible()) {
				f.setAccessible(true);
			}
			f.set(object, value);
		} catch (NoSuchFieldException e) {
			throw new RuntimeException(e);
		} catch (IllegalArgumentException e) {
			throw new RuntimeException(e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException(e);
		}
	}

	public static Object readField(Object object, String field) {
		try {
			Field f = getField(object.getClass(), field);
			if (!f.isAccessible()) {
				f.setAccessible(true);
			}
			return f.get(object);
		} catch (NoSuchFieldException e) {
			throw new RuntimeException(e);
		} catch (IllegalArgumentException e) {
			throw new RuntimeException(e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException(e);
		}
	}

	// HELPER METHODS ------------------------------------------------------------------------------

	private static Field getField(Class<?> clazz, String fieldName) throws NoSuchFieldException {
		synchronized (fieldMap) {
			FieldRefKey key = new FieldRefKey(clazz, fieldName);
			Field field = fieldMap.get(key);
			if (field == null) {
				field = findField(clazz, fieldName);
				fieldMap.put(key, field);
			}
			return field;
		}
	}

	private static Field findField(Class<?> clazz, String fieldName) throws NoSuchFieldException {
		for (Field field : clazz.getDeclaredFields()) {
			if (field.getName().equals(fieldName)) {
				return field;
			}
		}
		if (clazz.getSuperclass() != Object.class) {
			return findField(clazz.getSuperclass(), fieldName);
		}
		throw new NoSuchFieldException("Could not find field '" + fieldName + "'");
	}

	// INNER CLASSES -------------------------------------------------------------------------------

	/**
	 * Key that represents a combination of clazz and field name.
	 * @author Antonio Gomes
	 */
	private static final class FieldRefKey {

		private final Class<?> type;
		private final String fieldName;

		public FieldRefKey(Class<?> type, String fieldName) {
			super();
			this.fieldName = fieldName;
			this.type = type;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((fieldName == null) ? 0 : fieldName.hashCode());
			result = prime * result + ((type == null) ? 0 : type.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			FieldRefKey other = (FieldRefKey) obj;
			if (fieldName == null) {
				if (other.fieldName != null)
					return false;
			} else if (!fieldName.equals(other.fieldName))
				return false;
			if (type == null) {
				if (other.type != null)
					return false;
			} else if (!type.equals(other.type))
				return false;
			return true;
		}

	}

}
